Julia数据科学系列-TidierOrg

Tidier.jl

个人总结 用宏可以绕过Julia语法, 但宏导致很多julia的函数操作不能使用, 所以从短期层面, Tidier可以帮助R/Tidyverse用户更快适应julia, 但从长期角度, 反而不利于其习惯julia的Symbol、广播等操作,而且不太方便将复杂的自定义函数嵌入到Tidier宏操作中, 从长远深入使用julia的角度, 建议还是用DataFrame原生语法, 或者限制和改动没那么多的扩展包。
Tidier支持的宏 为了支持R风格编程, Tidier.jl用宏来进行操作, 目前支持以下宏和函数:

  • 顶层宏:

    • @glimpse()

    • @select(), @rename(), @distinct()

    • @mutate(), @transmute()

    • @summarize(), @summarise()

    • @filter(), @slice()

    • @group_by(), @ungroup()

    • @arrange()

    • @pull()

    • @count(), @tally()

    • @left_join(), @right_join(), @inner_join(), @full_join()

    • @bind_rows(), @bind_cols()

    • @pivot_wider(), @pivot_longer()

    • @drop_na()

    • @clean_names() (as in R's janitor::clean_names() function)

  • 辅助函数:

    • across()

    • desc()

    • if_else(), case_when()

    • n(), row_number(): 记录总行号和列号

    • ntile()

    • lag(), lead()

    • starts_with(), ends_with(), matches(), contains()

    • as_float(), as_integer(), as_string()

@select

  • 支持按名称或者数字索引列

  • 使用:进行范围索引(数字和名称)

  • 使用-!反向选择

  • 支持starts_with(), ends_with(), matches(), contains()匹配

julia

using Tidier
using RDatasets

movies = dataset("ggplot2", "movies")

# 按名称选择前5列
@chain movies begin
    @select(Title, Year, Length, Budget, Rating) # 按名称选择前5列
    @slice(1:5) # 提取前5行
end

# 使用范围选择
@chain movies begin
    @select(Title:Rating) # 按照在表格中出现的顺序指定范围
    @slice(1:5)
end

# 按索引选择前5列
@chain movies begin
    @select(1:5)
    @slice(1:5)
end

# 反向选择
# @select(-(Title:Rating))
# @select(!(Title:Rating))
# @select(-(1:5))

# 混合选择
# @select(1, Budget:Rating)

julia

@rename

  • 支持tidy表达式: new_name = old_name

  • 输出重命名后的全部的表格

  • 也可以用@select重命名(只会输出重命名的列)

julia

@chain movies begin
    @rename(title = Title, Minutes = Length)
    @select(Title = title, Length = Minutes)
    @slice(1:5)
end

julia

@mutate

  • 用于创建新的列或更新现有列(不改变行数), 输出更新后的全部表格

  • 也可以用@select或者@transmute只输出改变的列

julia

# 产生新列
@chain movies begin
    @filter(!ismissing(Budget))
    @mutate(Budget_Millions = Budget/1_000_000)
    @select(Title, Budget, Budget_Millions)
    @slice(1:5)
end

# 更新现有列
@chain movies begin
    @filter(!ismissing(Budget))
    @mutate(Budget = Budget/1_000_000) #覆盖Budget
    @select(Title, Budget)
    @slice(1:5)
end

# 配套in使用
@chain movies begin
  @filter(!ismissing(Budget))
  @mutate(Nineties = Year in 1990:1999)
  @select(Title, Year, Nineties)
  @slice(1:5)
end

# 配套n()和row_number()使用
@chain movies begin
  @mutate(Row_Num = row_number(),
          Total_Rows = n())
  @filter(!ismissing(Budget))
  @select(Title, Year, Row_Num, Total_Rows)
  @slice(1:5)
end

# 用@transmute更新和选择列(跟@select一样的)
@chain movies begin
    @filter(!ismissing(Budget))
    @transmute(Title = Title, Budget = Budget/1_000_000)
    @slice(1:5)
end

julia

@summarize

  • 将多行数据聚集成单行, 优先支持分组

  • 别名@summarise

  • 使用@summarize后不自动向量化

  • 使用@summarize后移除一层分组

julia

# 只有在@summarize中, n()会被转换为nrow()
@chain movies begin
    @summarize(n = n())
end

@chain movies begin
  @mutate(Budget = Budget / 1_000_000)
  @summarize(median_budget = median(skipmissing(Budget)),
             mean_budget = mean(skipmissing(Budget)))
end

# 分组统计
@chain movies begin
  @group_by(Year)
  @summarise(n = n())
  @arrange(desc(Year))
  @slice(1:5)
end

julia

@filter

  • 过滤行

  • 只保留计算结果为true的行

  • 跳过计算结果为missing的行

  • 支持&&, &和多表达式三种表示and的语法表示

    • &优先级高于>=, 所以要把&两侧的比较语句用()括起来, &&则不需要

julia

@chain movies begin
  @mutate(Budget = Budget / 1_000_000)
  @filter(Budget >= mean(skipmissing(Budget)))
  @select(Title, Budget)
  @slice(1:5)
end

# && vs &
@chain movies begin
  @filter(Votes >= 200 && Rating >= 8) # <--
  @select(Title, Votes, Rating)
  @slice(1:5)
end

@chain movies begin
  @filter((Votes >= 200) & (Rating >= 8)) # <--
  @select(Title, Votes, Rating)
  @slice(1:5)
end

# 多表达式
@chain movies begin
  @filter(Votes >= 200, Rating >= 8)
  @select(Title, Votes, Rating)
  @slice(1:5)
end

julia

@slice

  • 支持正负切片, 所以整数类型都是有效输入;

  • 优先处理分组;

  • 支持range()函数, 1:2:7等范围生成方法;

  • 逗号分隔多个选择表达式;

@group_by

  • 分组状态会一直保持, 直到遇到改变分组的宏;

  • @summarize(删除一层group)或者@ungroup(删除所有group)会改变分组状态;

  • 多列分组用,分开;

@arrange

  • 按指定列排序;

  • 默认升序, 用desc()包裹列名进行降序;

  • 对于分组数据排序时, @arrange会暂时取消分组, 然后执行sort(), 然后再重新分组;

julia

@chain movies begin
  @arrange(Year, desc(Rating))
  @select(1:5)
  @slice(1:5)
end

julia

@distinct

  • 按指定列去重;

  • 会返回所有列(与R中的不同, R里是只返回去重的列);

  • 优先考虑分组(把分组列当成去重列, 显式地传入, 应该是一样的效果吧?);

across

  • 用于在@mutate()@summarize()中操作多列或多个操作函数的辅助函数;

  • 语法across(列选择元组, 函数元组);

  • 列选择元组可以是对列名的辅助函数操作集合, 如(starts_with("Bud"), ends_with("ting"))

  • 函数元组支持融合函数: , 如mean(skipmissing())可以写作mean∘skipmissing

  • 函数元组支持匿名函数, 但目前版本中匿名函数内部不能写融合函数, 将在以后更新中修复该bug

julia

# 基本用法
@chain movies begin
  @mutate(Budget = Budget / 1_000_000)
  @summarize(across(Budget, mean∘skipmissing))
end

@chain movies begin
  @mutate(Budget = Budget / 1_000_000)
  @summarize(across(Budget, (x -> mean(skipmissing(x)))))
end

@chain movies begin
    @mutate(Budget = Budget / 1_000_000)
    @summarize(across((Rating, Budget), (mean∘skipmissing, median∘skipmissing)))
end

# 计算Rating和Budget的mean和median
@chain movies begin
    @mutate(Budget = Budget / 1_000_000)
    @summarize(across((Rating, Budget), (mean∘skipmissing, median∘skipmissing)))
end

# 辅助函数
@chain movies begin
  @mutate(Budget = Budget / 1_000_000)
  @summarize(across((starts_with("Bud"), ends_with("ting")), (mean∘skipmissing, median∘skipmissing)))
end

julia

ifelse()casewhen()

Tidier提供的两个条件处理函数。

if_else()

Tidier中的`if_else()`和julia自身的`ifelse()`

  • julia的ifelse()不支持缺失值;

  • Tidier的if_else()支持缺失值, 且可以设定缺失值的返回值

julia

df = DataFrame(a = [1, 2, missing, 4, 5])
# 缺失值变成"unknown"
@chain df begin
  @mutate(b = if_else(a >= 3, "yes", "no", "unknown"))
end
# 缺失默认还是missing
@chain df begin
  @mutate(b = if_else(a >= 3, 3, a))
end

julia

case_when()

需要评估多个条件时, if_else()就不太够用了, 这时用case_when()比较方便:

  • 语法: condition => return_value

julia

@chain df begin
    @mutate(b = case_when(a >= 3  =>  3,
                          true    =>  a))
end

julia

Joins

julia

# @left_join(df1, df2, [by])
@left_join(df1, df2, a1 = a2)
@left_join(df1, df2, "a1" = "a2")

# @right_join
# 略

# @inner_join
# 略

# @full_join
# 略

julia
多列join的思考 传统的多列join, 可以用reduce的思路实现, 但是由于Tidier用的是宏操作, 是不能用reduce进行操作的, 所以这种需求, 还是要用DataFrame自带的方法vcatreduce

Binding

@bind_rows(dfs..., [id])@bind_cols(dfs...)进行多个表格的行列拼接:

julia

df1 = DataFrame(a=1:3, b=1:3);
df2 = DataFrame(a=4:6, b=4:6);
df3 = DataFrame(a=7:9, c=7:9);
@bind_rows(df1, df2, df3, id = "id") # 把数据的原始所属表格存成id列
@bind_cols(df1, df2, df3) # 会自动把相同列名改成"raw_1","raw_2"

julia

Pivoting

长宽表转换

@pivot_wider(df, names_from = <xxx/"xxx">, values_from = <yyy/"yyy">)
@pivot_longer(df, <要透视的列名>), [names_to = <xxx/"xxx">, values_to = <yyy/"yyy">])

julia

# @pivot_wider() 长 -> 宽
df_long = DataFrame(id = [1, 1, 2, 2],
                    variable = ["A", "B", "A", "B"],
                    value = [1, 2, 3, 4])

@pivot_wider(df_long, names_from = variable, values_from = value)

# @pivot_longer() 宽 -> 长
df_wide = DataFrame(id = [1, 2], A = [1, 3], B = [2, 4])
@pivot_longer(df_wide, A:B) # 对A至B列进行数据透视
@pivot_longer(df_wide, -id) # 对除去id的其他列进行透视, 这样写貌似比较方便

julia

Column names

  • 列名默认是Tidy 语法: 列名的bareword;

  • 如果列名有空格, 则可以用var字符串宏: var"col name"

  • 或者可以用反引号

  • @clean_names(case = "camelCase")清理带空格的名字,默认是"snake_case"

julia

@chain df begin
  @mutate(var"age in 10 years" = var"my age" + 10)
end

@chain df begin
  @mutate(`age in 10 years` = `my age` + 10)
end

julia

Interpolation

  • 使用!!全局变量插入到代码中

  • 由于必须是全局变量, 所以在写代码时, 建议用@eval宏: @eval(Main, myvar = :b)

  • 在交互REPL中, 则直接赋值即可

  • 变量可以是单个字符或Symbol, 也可以是元组或列表

  • 如果在in中使用值是单个字符的变量插值,需要用[]包括, 不然会把字符展开成字母数组:

julia

@eval(Main, myvar_string = "b")
@chain df begin
    @filter(a in [!!myvar_string])
end

julia
  • 全局常量可以直接用!!访问, 如pi

  • !!不是必须的, 也可以用Main.pi来替代

自动向量化

  • mean()函数不会被向量化;

  • @summarize()中的函数不会自动向量化;

  • 任何其他函数都会自动向量化;

  • 使用~防止自动向量化;

  • 给不会被向量化的函数加上~, 也不影响结果;

  • 如果你变态到非要用mean()进行向量化计算, 可以用广播mean.(c)

  • 运算符也支持~操作: a ~* b将执行矩阵乘法(在@summarize外)

julia

df = DataFrame(a = repeat('a':'e', inner = 2), b = [1,1,1,2,2,2,3,3,3,4], c = 11:20)
new_mean(exprs...) = mean(exprs...)
# new_mean被向量化了, 结果是按元素计算均值, 等于没计算
@chain df begin
    @mutate(d = c - new_mean(c))
end
# 加上~, 就对了
@chain df begin
    @mutate(d = c - ~new_mean(c))
end

julia

其他函数和宏

Tidier_set

Tidier_set(option::AbstractString, value::Bool) 设置Tidier包的配置项:

  • code: 默认是false, 设置成true则会显示生Tidier宏生成的DataFrames.jl代码;

as_xxx

  • as_float(value): 将数字或字符串转成Float64

  • as_integer: Int64

  • as_string:

@tally

这个是@count()低阶辅助宏, 平时应该用不太到;

@tally(df, [wt], [sort]), 其中wt表示权重列(计算该列总和, 而不是计数), sort::Bool表示是否对计数排序

@count

@count(df, exprs..., [wt], [sort=true/false])

ntile()

  • 将输入向量n等分, 如果不是n的倍数, 则bin大小最多差1个,大bin在前

  • 可以理解为一个粗略的排名函数, 但是会忽略平局的情况

@drop_na

@drop_na(df, [cols...])
  • 不带参数调用, 删除任何具有缺失的行

  • 指定列作为参数, 则只删除指定列中有缺失的行

@glimpse

@glimpse(df, width = 80)

预览表格, 根据width设置输出宽度(字符数)

@pull

@pull(df, col)
提取一列作为向量

TiderPlots.jl

TBC...